Системное программирование

Системные вызовы в Linux

Системное программирование

Системные вызовы в Linux: Введение в системное программирование в Linux

Сегодня мы поговорим о системных вызовах в Linux. Это фундаментальная концепция системного программирования, которая лежит в основе взаимодействия пользовательского кода с ядром операционной системы.

Системные вызовы в Linux
Системное программирование

План лекции:

  1. Что такое системные вызовы?
  2. Зачем нужны системные вызовы?
  3. Механизм работы системных вызовов.
  4. Типы системных вызовов.
  5. Примеры системных вызовов (с акцентом на наиболее важные).
  6. Как использовать системные вызовы из C.
  7. Обработка ошибок системных вызовов.
  8. Системные вызовы и библиотеки C (glibc).
  9. Системные вызовы и безопасность.
  10. Альтернативы системным вызовам.
  11. Демонстрация использования системных вызовов.
  12. Заключение.
Системные вызовы в Linux
Системное программирование

1. Что такое системные вызовы?

Системный вызов (system call) – это механизм, с помощью которого программа, выполняющаяся в пользовательском режиме, запрашивает у ядра операционной системы выполнение привилегированной операции. Ядро обладает прямым доступом к аппаратным ресурсам и может выполнять операции, недоступные пользовательским процессам.

Представьте себе, что вы хотите напечатать текст на экране. Ваш процесс (в пользовательском режиме) не может напрямую управлять видеокартой. Он должен запросить у ядра, чтобы оно это сделало за него. Этот запрос и есть системный вызов.

Системные вызовы в Linux
Системное программирование

2. Зачем нужны системные вызовы?

Системные вызовы обеспечивают:

  • Безопасность: Пользовательские процессы не могут напрямую изменять конфигурацию системы или получить доступ к данным других процессов. Вместо этого, они должны пройти через контролируемый интерфейс ядра.
  • Абстракцию: Системные вызовы предоставляют унифицированный интерфейс для доступа к аппаратным ресурсам, независимо от их конкретной реализации. Например, независимо от производителя вашей видеокарты, для вывода текста на экран вы используете (через библиотеки) одни и те же системные вызовы.
  • Разделение привилегий: Только ядро имеет право выполнять определенные операции, такие как управление памятью, ввод-вывод, управление процессами. Системные вызовы позволяют пользователям запрашивать эти операции, но только в соответствии с правилами, установленными ядром.
  • Согласованность: Системные вызовы гарантируют, что операции выполняются последовательно и предсказуемо, избегая конфликтов между процессами.
Системные вызовы в Linux
Системное программирование

3. Механизм работы системных вызовов.

Механизм системного вызова состоит из нескольких этапов:

  1. Программа (в пользовательском режиме) вызывает функцию из стандартной библиотеки C (например, printf, которая в итоге использует write). Эта функция, в свою очередь, вызывает системный вызов.
  2. Параметры системного вызова помещаются в регистры процессора (или в стек, если параметров слишком много). Номер системного вызова (уникальный идентификатор) также помещается в регистр.
  3. Выполняется специальная машинная команда, называемая "trap" или "software interrupt" (например, int 0x80 в старых системах x86 или syscall в современных x86-64). Эта команда переключает процессор в режим ядра (kernel mode).
  4. Ядро (в режиме ядра) обрабатывает прерывание, вызванное "trap". Оно находит соответствующую функцию в таблице системных вызовов, основываясь на номере системного вызова.
  5. Ядро выполняет запрошенную операцию, используя параметры, переданные через регистры.
  6. Ядро возвращает результат выполнения в регистр процессора.
  7. Выполняется команда возврата из прерывания (return from interrupt), которая переключает процессор обратно в пользовательский режим.
  8. Функция в пользовательском режиме получает результат системного вызова и возвращает его программе.
Системные вызовы в Linux
Системное программирование

4. Типы системных вызовов.

Системные вызовы можно классифицировать по их функциональности:

  • Управление процессами: fork, exec, wait, exit, kill, getpid, getuid, setuid.
  • Управление файловой системой: open, close, read, write, lseek, stat, mkdir, rmdir, unlink, chmod, chown.
  • Управление памятью: mmap, munmap, brk, sbrk.
  • Управление сетью: socket, bind, listen, accept, connect, send, recv.
  • Управление сигналами: signal, sigaction, kill.
  • Разное: ioctl, time, sleep, uname.
Системные вызовы в Linux
Системное программирование

5. Примеры системных вызовов (с акцентом на наиболее важные).

  • fork(): Создает новый процесс, который является точной копией вызывающего процесса (родительского). Возвращает PID нового процесса в родительском процессе и 0 в дочернем процессе. При ошибке возвращает -1. fork() часто используется для создания параллельных процессов.
  • exec(): Заменяет текущий процесс новым образом программы. В отличие от fork(), который создает новый процесс, exec() не создает новый процесс, а перезаписывает текущий. Существуют различные варианты exec(): execl, execv, execle, execve, execlp, execvp.
  • open(): Открывает файл. Принимает имя файла, флаги (режим открытия, например, O_RDONLY, O_WRONLY, O_CREAT), и, опционально, права доступа (если файл создается). Возвращает файловый дескриптор (неотрицательное целое число) или -1 в случае ошибки.
Системные вызовы в Linux
Системное программирование
  • read(): Читает данные из файла. Принимает файловый дескриптор, указатель на буфер, и количество байт, которые нужно прочитать. Возвращает количество фактически прочитанных байт или -1 в случае ошибки.
  • write(): Записывает данные в файл. Принимает файловый дескриптор, указатель на буфер, и количество байт, которые нужно записать. Возвращает количество фактически записанных байт или -1 в случае ошибки.
  • close(): Закрывает файл. Принимает файловый дескриптор. Возвращает 0 при успехе или -1 в случае ошибки.
  • exit(): Завершает процесс. Принимает код возврата, который может быть использован родительским процессом для определения причины завершения.
Системные вызовы в Linux
Системное программирование

6. Как использовать системные вызовы из C.

Обычно напрямую системные вызовы не вызываются. Вместо этого используются функции стандартной библиотеки C (glibc), которые оборачивают системные вызовы. Например:

  • Вместо прямого использования системного вызова write используется функция printf (или fwrite).
  • Вместо прямого использования системного вызова exit используется функция exit.

Тем не менее, в некоторых случаях необходимо напрямую использовать системные вызовы. Для этого используется функция syscall(). Она принимает номер системного вызова и его аргументы.

Системные вызовы в Linux
Системное программирование
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <errno.h>

int main() {
    long result = syscall(SYS_write, 1, "Hello, syscall!\n", 16); // SYS_write - номер системного вызова write

    if (result == -1) {
        perror("syscall"); // Выводим описание ошибки
        return 1;
    }

    printf("Записано %ld байт\n", result);

    return 0;
}

В этом примере мы напрямую вызываем системный вызов write для вывода текста на стандартный вывод (файловый дескриптор 1). Номер системного вызова SYS_write определяется в заголовочном файле <sys/syscall.h>.

Системные вызовы в Linux
Системное программирование

7. Обработка ошибок системных вызовов.

Системные вызовы могут завершиться с ошибкой. В случае ошибки системный вызов обычно возвращает -1 (или другое специальное значение, зависящее от конкретного вызова) и устанавливает глобальную переменную errno в код ошибки.

Важно всегда проверять возвращаемое значение системного вызова и обрабатывать ошибки соответствующим образом. Для этого можно использовать функцию perror() для вывода описания ошибки на стандартный поток ошибок.

Системные вызовы в Linux
Системное программирование
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main() {
    int fd = open("nonexistent_file.txt", O_RDONLY);
    if (fd == -1) {
        perror("open"); // Вывод сообщения об ошибке, связанной с системным вызовом open
        printf("errno = %d\n", errno); // Вывод кода ошибки
        return 1;
    }

    // ... дальнейшая работа с файлом ...

    close(fd);
    return 0;
}
Системные вызовы в Linux
Системное программирование

8. Системные вызовы и библиотеки C (glibc).

Большинство программистов взаимодействуют с ядром Linux через функции стандартной библиотеки C (glibc). glibc предоставляет обертки для системных вызовов, которые упрощают их использование.

Например, функция printf из glibc использует системный вызов write для вывода текста на экран. Функция fopen использует системные вызовы open и fstat.

glibc также предоставляет дополнительные функции, которые не являются прямыми обертками для системных вызовов, но используют их для своей реализации (например, malloc, free, qsort).

Системные вызовы в Linux
Системное программирование

Преимущества использования glibc:

  • Переносимость: Код, использующий glibc, более переносим между разными Unix-подобными системами.
  • Удобство: Функции glibc часто предоставляют более удобный интерфейс, чем прямые системные вызовы.
  • Оптимизация: glibc может оптимизировать системные вызовы для конкретной аппаратной платформы.
Системные вызовы в Linux
Системное программирование

9. Системные вызовы и безопасность.

Системные вызовы играют важную роль в обеспечении безопасности системы. Ядро выполняет проверку прав доступа перед выполнением каждого системного вызова. Это предотвращает несанкционированный доступ к ресурсам системы.

Однако, некорректное использование системных вызовов может привести к уязвимостям. Например, уязвимость "race condition" может возникнуть, если два процесса одновременно пытаются изменить один и тот же файл. Некорректная проверка параметров системного вызова также может привести к уязвимостям.

Системные вызовы в Linux
Системное программирование

10. Альтернативы системным вызовам.

В некоторых случаях можно избежать прямого использования системных вызовов. Например, можно использовать стандартные потоки ввода-вывода (stdin, stdout, stderr) вместо прямого использования системных вызовов read и write.

Также существуют альтернативные библиотеки, которые предоставляют более высокоуровневый интерфейс для взаимодействия с операционной системой.

Системные вызовы в Linux
Системное программирование

11. Демонстрация использования системных вызовов.

Напишем программу, которая копирует файл с использованием системных вызовов open, read, write, close.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define BUFFER_SIZE 4096

int main(int argc, char *argv[]) {
    int source_fd, dest_fd;
    ssize_t bytes_read, bytes_written;
    char buffer[BUFFER_SIZE];

    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source_file> <destination_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
Системные вызовы в Linux
Системное программирование
    // Открываем исходный файл для чтения
    source_fd = open(argv[1], O_RDONLY);
    if (source_fd == -1) {
        perror("open source");
        exit(EXIT_FAILURE);
    }

    // Открываем целевой файл для записи (создаем, если не существует, обрезаем, если существует)
    dest_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644); // 0644 - права доступа к файлу (rw-r--r--)
    if (dest_fd == -1) {
        perror("open destination");
        close(source_fd); // Не забываем закрыть исходный файл, если не удалось открыть целевой
        exit(EXIT_FAILURE);
    }
Системные вызовы в Linux
Системное программирование
    // Копируем данные из исходного файла в целевой
    while ((bytes_read = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
        bytes_written = write(dest_fd, buffer, bytes_read);
        if (bytes_written != bytes_read) {
            perror("write");
            close(source_fd);
            close(dest_fd);
            exit(EXIT_FAILURE);
        }
    }

    if (bytes_read == -1) {
        perror("read");
        close(source_fd);
        close(dest_fd);
        exit(EXIT_FAILURE);
    }

    // Закрываем файлы
    if (close(source_fd) == -1) {
        perror("close source");
        close(dest_fd);
        exit(EXIT_FAILURE);
    }

    if (close(dest_fd) == -1) {
        perror("close destination");
        exit(EXIT_FAILURE);
    }

    printf("Файл успешно скопирован.\n");
    return 0;
}
Системные вызовы в Linux
Системное программирование

12. Заключение.

Системные вызовы – это важная концепция системного программирования, которая позволяет пользовательским процессам взаимодействовать с ядром операционной системы. Понимание работы системных вызовов необходимо для написания эффективных и безопасных программ. Хотя чаще всего системные вызовы используются через обертки библиотек C, знание механики их работы необходимо для глубокого понимания работы операционной системы.

Системные вызовы в Linux